Clasificación clientes campaña bancaria

Modelo predictivo para clasificar los clientes de un banco Portugues

Notebook por Alfredo Pasmiño

Basado en el problema de clasificación de clientes de una campaña de marketing telefónico de un banco portugues, realizaremos un modelo para clasificar correctamente la variable a precir si el cliente acepta o no el producto que se le ofrece.

Más información https://archive.ics.uci.edu/ml/datasets/bank+marketing

1 Librerías

In [2]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import cm as cm
import seaborn as sns
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, StandardScaler
from sklearn.naive_bayes import GaussianNB
from sklearn.utils import resample
from sklearn.model_selection import (cross_val_score, cross_val_predict, RandomizedSearchCV, ShuffleSplit, 
                                     KFold, StratifiedKFold)
from sklearn.metrics import (confusion_matrix, precision_recall_fscore_support, mean_squared_error, roc_curve, auc, 
                             classification_report, accuracy_score, make_scorer, precision_recall_curve,recall_score, 
                             f1_score) 
from sklearn.linear_model import LogisticRegression
from sklearn import linear_model, datasets, cross_validation, metrics
from xgboost import XGBClassifier
from sklearn.model_selection import learning_curve
from scipy import interp

2. Análisis Exploratorio

Como primer paso Leemos el archivo con los datos, verificamos el tipo de dato de cada variable y visualizamos algunos estadísticas básicas del dataframe. para ello leerremos un archivo en formato txt como primera aproximación al modelo final en productivo.

In [1]:
#ruta del archivo con los datos
ruta="C:/Users/alfredo/Documents/Diplomado"
In [4]:
df=pd.read_csv(ruta+"/bank-full.csv", sep=";", decimal=".",  header=0)
In [5]:
#ruta del archivo con los datos
df.head()
Out[5]:
age job marital education default balance housing loan contact day month duration campaign pdays previous poutcome y
0 58 management married tertiary no 2143 yes no unknown 5 may 261 1 -1 0 unknown no
1 44 technician single secondary no 29 yes no unknown 5 may 151 1 -1 0 unknown no
2 33 entrepreneur married secondary no 2 yes yes unknown 5 may 76 1 -1 0 unknown no
3 47 blue-collar married unknown no 1506 yes no unknown 5 may 92 1 -1 0 unknown no
4 33 unknown single unknown no 1 no no unknown 5 may 198 1 -1 0 unknown no
In [6]:
#verificamos el número de filas y columnas
df.shape
Out[6]:
(45211, 17)

El dataframe contiene 45.211 filas y 17 columnas

In [7]:
#verificamos los tipos de datos
df.info()

RangeIndex: 45211 entries, 0 to 45210
Data columns (total 17 columns):
age          45211 non-null int64
job          45211 non-null object
marital      45211 non-null object
education    45211 non-null object
default      45211 non-null object
balance      45211 non-null int64
housing      45211 non-null object
loan         45211 non-null object
contact      45211 non-null object
day          45211 non-null int64
month        45211 non-null object
duration     45211 non-null int64
campaign     45211 non-null int64
pdays        45211 non-null int64
previous     45211 non-null int64
poutcome     45211 non-null object
y            45211 non-null object
dtypes: int64(7), object(10)
memory usage: 5.9+ MB
In [8]:
df.describe()
Out[8]:
age balance day duration campaign pdays previous
count 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000
mean 40.936210 1362.272058 15.806419 258.163080 2.763841 40.197828 0.580323
std 10.618762 3044.765829 8.322476 257.527812 3.098021 100.128746 2.303441
min 18.000000 -8019.000000 1.000000 0.000000 1.000000 -1.000000 0.000000
25% 33.000000 72.000000 8.000000 103.000000 1.000000 -1.000000 0.000000
50% 39.000000 448.000000 16.000000 180.000000 2.000000 -1.000000 0.000000
75% 48.000000 1428.000000 21.000000 319.000000 3.000000 -1.000000 0.000000
max 95.000000 102127.000000 31.000000 4918.000000 63.000000 871.000000 275.000000
In [9]:
#revisamos si existen missing

df.isnull().sum().sum()
Out[9]:
0

El dataframe no tiene datos missing

In [43]:
%matplotlib inline
sns.set()
df[df.dtypes[(df.dtypes=="float64")|(df.dtypes=="int64")].index.values].hist(figsize=[14,14])
Out[43]:
array([[,
        ,
        ],
       [,
        ,
        ],
       [,
        ,
        ],
       [,
        ,
        ]], dtype=object)

Revisamos los histogramas y podemos verificar cada una de las variables, su distribución y si presenta outliers en cada variable.

In [11]:
#graficamos las variables categóricas

fig, axes =plt.subplots(4,3, figsize=(12,12), sharex=True)
axes = axes.flatten()
object_bol = df.dtypes == 'object'
for ax, catplot in zip(axes, df.dtypes[object_bol].index):
    sns.countplot(y=catplot, data=df, ax=ax)

plt.tight_layout()  
plt.show()
In [12]:
#correlación

df.corr()
Out[12]:
age balance day duration campaign pdays previous
age 1.000000 0.097783 -0.009120 -0.004648 0.004760 -0.023758 0.001288
balance 0.097783 1.000000 0.004503 0.021560 -0.014578 0.003435 0.016674
day -0.009120 0.004503 1.000000 -0.030206 0.162490 -0.093044 -0.051710
duration -0.004648 0.021560 -0.030206 1.000000 -0.084570 -0.001565 0.001203
campaign 0.004760 -0.014578 0.162490 -0.084570 1.000000 -0.088628 -0.032855
pdays -0.023758 0.003435 -0.093044 -0.001565 -0.088628 1.000000 0.454820
previous 0.001288 0.016674 -0.051710 0.001203 -0.032855 0.454820 1.000000
In [13]:
#matriz de correlación

plt.rcParams['figure.figsize'] = (15.0, 6.0)
corr=df[['age','balance','day','duration','campaign','pdays','previous']].corr()
cmap = cm.get_cmap('seismic', 50)
sns.heatmap(corr, linewidths=.5, cmap=cmap)
Out[13]:

Revisamos la correlación entre variables y verificamos que las variables no estan correlacionadas, la única que tiene una correlación positiva pero debil es "Previous" y "Pday".

In [14]:
#graficamos la poporción de variable a predecir

print(df['y'].value_counts())
plt.figure(figsize=(8,5))
ax = sns.countplot(x='y', data=df)
no     39922
yes     5289
Name: y, dtype: int64

Volvemos a revisar la variable a predecir "Y" y podemos verificar que tenemos un problema de desbalance por lo que nuestro modelo no será muy bueno al precir los clientes que si aceptaron el producto, para ellos utilizaremos un resampling para que nustra proporción de "si" y "no" sea lo mas parejo posible.

3. Feature engineering

In [15]:
#label encoder

le=LabelEncoder()

for col in df[['default', 'housing', 'loan', 'y']]:
        data=df[col].append(df[col])
        le.fit(data.values)
        df[col]=le.transform(df[col])

para las variables categóricas con solo 2 tipos de categorías codificamos sus variables y revisamos el resultado.

In [16]:
#revisamos el resultado
df[['default', 'housing', 'loan', 'y']].head()
Out[16]:
default housing loan y
0 0 1 0 0
1 0 1 0 0
2 0 1 1 0
3 0 1 0 0
4 0 0 0 0

Para las variables categóricas con mutiples categorias aplicamos one hot encoding por lo que nuestro número de columnas aumenta a 49, revisaremos como quedan en la siguiente muestra.

In [17]:
#one hot encoder

col_transformar=['job', 'marital', 'education', 'contact', 'month', 'poutcome']
df = pd.get_dummies(df, columns = col_transformar, sparse=True)
In [31]:
df.shape
Out[31]:
(45211, 49)
In [18]:
df.head()
Out[18]:
age default balance housing loan day duration campaign pdays previous ... month_jun month_mar month_may month_nov month_oct month_sep poutcome_failure poutcome_other poutcome_success poutcome_unknown
0 58 0 2143 1 0 5 261 1 -1 0 ... 0 0 1 0 0 0 0 0 0 1
1 44 0 29 1 0 5 151 1 -1 0 ... 0 0 1 0 0 0 0 0 0 1
2 33 0 2 1 1 5 76 1 -1 0 ... 0 0 1 0 0 0 0 0 0 1
3 47 0 1506 1 0 5 92 1 -1 0 ... 0 0 1 0 0 0 0 0 0 1
4 33 0 1 0 0 5 198 1 -1 0 ... 0 0 1 0 0 0 0 0 0 1

5 rows × 49 columns

Artificialmente generaremos muestras para la categoría minoritaria para balancear la variable a predecir

In [19]:
#separamos la clase mayoritaria y minoritaria

df_majority = df[df.y==0]
df_minority = df[df.y==1]
 
#generamos más datos artificialmente para la clase minoritaria
df_minority_upsampled = resample(df_minority, 
                                 replace=True,     
                                 n_samples=39922,    
                                 random_state=123) 
 

#combinamos la clase minoritaria con la generada mayoritaria
df_upsampled = pd.concat([df_majority, df_minority_upsampled])

 
# mostramos la nueva clase
print(df_upsampled.y.value_counts())
1    39922
0    39922
Name: y, dtype: int64
In [20]:
#selecciono las variables para construir el modelo

y = df_upsampled.y
X = df_upsampled.drop('y', axis=1)

Como tenemos variables con diferentes unidades de medida estandarizaremos las variables.

In [21]:
#estandarizo las variables

seed = 10
scaler = StandardScaler()
scaler.fit(X)
X =scaler.transform(X)

4. Funciones

Creamos algunas funciones más complejas con nuestro dataset y que nos seviran más adelante.

La función plot_confusionMatrix recibe como parámetro de ingreso la variable "target" clasificadas en el modelo y la variable "prediction" que son las etiquetas en la muestra de testing.

In [38]:
def plot_confusionMatrix(targets, predictions, target_names=['No acepta', 'Acepta'], cmap="YlGnBu"):
    """
    Función que grafica la matriz de confusión
    """
    cm = confusion_matrix(targets, predictions)
 
    dfcm = pd.DataFrame(data=cm, columns=target_names, index=target_names)
    plt.figure(figsize=(10,6))
    plt.title('Matriz de confusión')
    
    sns.heatmap(dfcm, annot=True, fmt="d", linewidths=.5, cmap=cmap)

la función classificationReport retorna las métricas basadas en los resultados de cada modelo.

In [23]:
def classificationReport(y_true, y_pred):
    """
    función que entrega el accuracy
    """
    originalclass.extend(y_true)
    predictedclass.extend(y_pred)
    
    #retorno las métricas accuracy, precision, recall
    return accuracy_score(y_true, y_pred)

La función plot_classificationReport recibe como parámetros las métricas del reporte de clasificación basados en el score.

In [24]:
def plot_classificationReport(y_true, y_pred, figsize=(10, 6), ax=None):
    
    """
    función que grafica las métricas precision, recall y f1 score
    """
    plt.figure(figsize=figsize)
    plt.title('Métricas modelo')
    xticks = ['Precision', 'Recall', 'f1-score']
    yticks=['No acepta', 'Acepta']
    rep = np.array(precision_recall_fscore_support(y_true, y_pred)).T
    
    sns.heatmap(rep[:,:-1], annot=True, cbar=True, xticklabels=xticks, yticklabels=yticks, ax=ax, cmap="RdBu")

La funcion plot_learning_curve recibe como parámetros de entrada el modelo a usar y las variables "X" e "y", retornando el gráfico.

In [25]:
def plot_learning_curve(estimator, X, y, ylim=None, cv=None,
                        n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):
    """
     Función que grafica la curva learning rate training score vs cross validation
    """
    plt.figure()
    if ylim is not None:
        plt.ylim(*ylim)
    plt.xlabel("Muestras de entrenamiento")
    plt.ylabel("Score")
    plt.title("Learning Curves")
    train_sizes, train_scores, test_scores = learning_curve(
        estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes)
    train_scores_mean = np.mean(train_scores, axis=1)
    train_scores_std = np.std(train_scores, axis=1)
    test_scores_mean = np.mean(test_scores, axis=1)
    test_scores_std = np.std(test_scores, axis=1)
    plt.grid()

    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
                     train_scores_mean + train_scores_std, alpha=0.1,
                     color="r")
    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
                     test_scores_mean + test_scores_std, alpha=0.1, color="g")
    plt.plot(train_sizes, train_scores_mean, 'o-', color="r",
             label="Training score")
    plt.plot(train_sizes, test_scores_mean, 'o-', color="g",
             label="Cross-validation score")

    plt.legend(loc="best")
    return plt

La función plotROC recibe como parámetros el modelo seleccionado, la variable "X" que son las variables independientes de nuestro modelo y la variable "y" la variable dependiente a predecir.

In [48]:
def plotROC(model, X, y):
   
    """
     Función que grafica la curva de ROC
    """   
    
    clf = model
    x_train, x_test, y_train, y_test = cross_validation.train_test_split(X, y)
    clf.fit(x_train, y_train)
    y_pred = clf.predict(x_test)
    fpr, tpr, thresholds = roc_curve(y_test, y_pred)
    roc_auc = auc(fpr, tpr)

    plt.title('Receiver Operating Characteristic')
    plt.plot(fpr, tpr, label='AUC = %0.4f'% roc_auc)
    plt.legend(loc='lower right')
    plt.plot([0,1],[0,1],'r--')
    plt.xlim([-0.001, 1])
    plt.ylim([0, 1.001])
    plt.ylabel('True Positive Rate')
    plt.xlabel('False Positive Rate')
    plt.show()

Prepararemos los modelos seleccionando un set de parámetros (kernel, regularización, etc.) y para tener el mejor ajuste usaremos la función RandomsearchCV.

5. Constucción y evaluación del modelo

In [27]:
#preparo los modelos

param_grid_xgb = {'n_estimators':[50,100,150,200],
 'max_depth':[2,3,4,5,6,7,8,9],
 'min_child_weight':[2,3,4,5],
 'colsample_bytree':[0.2,0.6,0.8],
 'colsample_bylevel':[0.2,0.6,0.8]}

    
param_grid_lr = {'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000] }
In [28]:
#seleccionamos el mejor parámetro para regresión logística
lr=LogisticRegression()
estimacion_lr = RandomizedSearchCV(estimator=lr, n_iter=7,  param_distributions=param_grid_lr, cv= 5)
estimacion_lr.fit(X, y)
print (estimacion_lr.best_params_)
{'C': 0.001}
In [29]:
#seleccionamos el mejor parámetro para xgboost

xg=XGBClassifier()
estimacion_xgb = RandomizedSearchCV(estimator=xg,   param_distributions=param_grid_xgb, cv= 5, n_iter=5)
estimacion_xgb.fit(X, y)
print (estimacion_xgb.best_params_)
{'n_estimators': 150, 'min_child_weight': 4, 'max_depth': 9, 'colsample_bytree': 0.2, 'colsample_bylevel': 0.8}
In [30]:
#guardo los modelos

modelos = []
modelos.append(('GNB', GaussianNB()))
modelos.append(('LR', LogisticRegression(C=0.001)))
modelos.append(('xbg', XGBClassifier()))

Para evaluar el modelo usaremos como muestra de entrenamiento un 75% y de validación un 25%. Usaremos validación cruzada con 10 iteraciones con muestras al azar por cada iteración.

In [31]:
#evalúo el modelo

resultados=[]
nombres=[]
scoring = 'accuracy'
originalclass = []
predictedclass = []

for nombre, modelo in modelos:
    shuffle_split = ShuffleSplit(test_size=.25, n_splits=10, random_state=seed)
    cv_resultados = cross_val_score(modelo, X, y, cv=shuffle_split, scoring=make_scorer(classificationReport))
    resultados.append(cv_resultados)
    nombres.append(nombre)
    msg = ("%s %f (%f)" % (nombre, cv_resultados.mean(), cv_resultados.std()))
    print(msg)
GNB 0.713727 (0.004393)
LR 0.832574 (0.002270)
xbg 0.859336 (0.002085)

El resultado de los modelos tenemos que el mejor accuracy lo entrega el modelo de gradient boosting con un 85,6% aproximado de acierto correctos, en segundo lugar se encuentra la regresión logistica y por último el método de Bayes, en este caso el método de boosting es el que seleccionaremos para usar en nuestro modelo, a continuación veremos otras métricas por cada modelo.

In [32]:
#graficamos el acuracy en los 10 k fold-cross validation

plt.plot(resultados[0], label='SVC')
plt.plot(resultados[1], label='LG')
plt.plot(resultados[2], label='XGB')
plt.ylabel('Accuray')
plt.xlabel('Num k fold')
plt.title('Comparación algoritmos / Num k fold')
plt.legend(loc=3)
plt.show()

El gráfico muestra el accuracy y como se comporta en las 10 iteraciones considerando la validación cruzada, además se puede apreciar que el método de Gradient boosting siempre tiene una mejor performance comparando los otros modelos por lo que su capacidad de clasificación es mejor en todas las iteraciones.

In [34]:
nClass=[] 
for i in range(0, len(modelos)+1):         
    nClass.append(len(originalclass)/(len(modelos))*i )
    i=+1

#métricas de regresión logística
%matplotlib inline

plot_confusionMatrix(originalclass[int(nClass[0]):int(nClass[1])], predictedclass[int(nClass[0]):int(nClass[1])])

classificationReport=(classification_report(originalclass[int(nClass[0]):int(nClass[1])], 
                                            predictedclass[int(nClass[0]):int(nClass[1])]))

print((classificationReport))

plot_classificationReport(originalclass[int(nClass[0]):int(nClass[1])], predictedclass[int(nClass[0]):int(nClass[1])])
             precision    recall  f1-score   support

          0       0.66      0.87      0.75     99697
          1       0.81      0.56      0.66     99913

avg / total       0.74      0.71      0.71    199610

Al revisar las métricas de la matriz de confusión del modelo de Bayes, predijo que en 10 iteraciones 86.906 clientes no aceptaron el producto, estos fueron correctamete clasificados es decir son los verdaderos positivos y fueron mal clasificados 2.057 clientes como falso negativo. los verdaderos negativo que son 55.561 clientes clasificados correctamente aceptando el producto y los falsos positivos son 44.352 clientes mal clasificados. la exactitud total del modelo (overall accuracy) corresponde: $(Verdaderos\space positivo + Verdaderos\space negativo)/\space Total$ . Resultando un 74%. resultado que revisamos en el paso anterior al comparar cada modelo.

La otras métricas son precision que es el ratio de como clasificó correctamente observaciones positivas es decir $Verdaderos\space positivos / (Verdaderos\space positivos + Falsos\space positivos)$ en este caso es de 66% para los clientes que no aceptan y un 81% para los clientes que aceptan. Recall es otro ratio que mide los eventos clasificados positivamente como Verdaderos positivos/(Verdaderos positivos+Falsos negativos)Verdaderos positivos/(Verdaderos positivos+Falsos negativos) en este caso es de 87% para los clientes que no aceptan y un 56% para los que aceptan el producto.

In [35]:
#métricas de SVM
plot_confusionMatrix(originalclass[int(nClass[1]):int(nClass[2])], predictedclass[int(nClass[1]):int(nClass[2])])

classificationReport=classification_report(originalclass[int(nClass[1]):int(nClass[2])], 
                                           predictedclass[int(nClass[1]):int(nClass[2])])
print(classificationReport)
plot_classificationReport(originalclass[int(nClass[1]):int(nClass[2])], predictedclass[int(nClass[1]):int(nClass[2])])
             precision    recall  f1-score   support

          0       0.82      0.85      0.84     99697
          1       0.85      0.81      0.83     99913

avg / total       0.83      0.83      0.83    199610

La matriz de confusión en el modelo de regresión logística, este predijo en 10 iteraciones que 84.886 clientes "no eceptaron" el producto fueron correctamete clasificados es decir son los verdaderos positivos y fueron mal clasificados 14.811 clientes como falso negativo. los verdaderos negativo que son 81.304 clientes clasificados correctamente como "aceptaron" y los falsos positivos son 18.609 clientes mal clasificados. la exactitud total del modelo (overall accuracy) corresponde a 83%. la precision es de 82% para los que no aceptan y 85% para los que aceptan el producto. El recall es de 85% para los que no aceptan y 81% para los que aceptan el producto. El resultado es mucho mejor comparandolo con bayes en especial a nosotros nos interesa que clasifique de mejor manera los clientes que aceptan.

In [36]:
#métricas de bosques aleatorios
plot_confusionMatrix(originalclass[int(nClass[2]):int(nClass[3])], predictedclass[int(nClass[2]):int(nClass[3])])

classificationReport=classification_report(originalclass[int(nClass[2]):int(nClass[3])], 
                                           predictedclass[int(nClass[2]):int(nClass[3])])
print(classificationReport)
plot_classificationReport(originalclass[int(nClass[2]):int(nClass[3])], predictedclass[int(nClass[2]):int(nClass[3])])
             precision    recall  f1-score   support

          0       0.88      0.83      0.86     99697
          1       0.84      0.89      0.86     99913

avg / total       0.86      0.86      0.86    199610

La matriz de confusión en el modelo de gradian boosting, este predijo en 10 iteraciones que 82.789 clientes "no aceptaron" el prodcuto fueron correctamete clasificados es decir son los verdaderos positivos y fueron mal clasificados 16.908 clientes como falso negativo. los verdaderos negativo que son 88.743 clientes clasificados correctamente como "aceptaron" y los falsos positivos son 11.170 clientes mal clasificados. la exactitud total del modelo (overall accuracy) corresponde a 86%. la precision es de 88% para los que no aceptan y 84% para los que aceptan el producto. El recall es de 83% para los que no aceptan y 89% para los que aceptan el producto. Comparandolo con regresión logistica este modelo tiene una mejor capacidad para clasificar los clientes que "aceptan" el producto y si consideramos el promedio total de la precisión nos quedaremos finalmente con el método de gradian boosting.

In [33]:
#graficamos la curva de aprendizaje

cv = ShuffleSplit(test_size=.25, n_splits=10, random_state=seed)
estimator = modelos[2][1]
estimator.fit(X, y)
plot_learning_curve(estimator, X, y, (0.7, 1.01), cv=cv, n_jobs=4)
plt.show()

El gráfico de la curva de aprendisaje se puede visualizar como se comporta el modelo de gradiant boosting entre el puntaje con la muestra de entrenamiento y el puntaje de validación cruzada, y esta se basa en la cantidad de muestras, se puede apreciar que el modelo es bastante robusto ya que no experimenta mayores cambios en ambas curvas y la distancia entre ambas es muy pequeña por lo que no es necesario realizar algún ajuste (tomar mas muestras de entrenamiento o aplicar alguna técnica para reducir variables).

In [49]:
proba=plotROC(modelos[2][1], X, y)

La curva de ROC, para el modelo final nos entrega el área bajo la curva y este gráfico es la representación de la razón o ratio de verdaderos positivos (VPR = Razón de Verdaderos Positivos) frente a la razón o ratio de falsos positivos (FPR = Razón de Falsos Positivos) así tenemos el umbral de clasificación de un 85.8%, este resultado significa la capacidad para discriminar entre las dos clases que tenemos y es mucho mejor que dejarlo al azar que sería equivalente a un 50%.

6. Conclusión

Para concluir, luego del análisis exploratorio de las variables, la codificación de las etiquetas y el uso de técnicas como one hot encoding para usarlo en modelos como regresión logística tenemos un resultado muy bueno ya que nuestro modelo tiene la capacidad de predicción del 85.8%, luego de balancear la variable a predecir y de probar diferentes modelos, el mejor resultado nos da el modelo de Gradient boosting con el que finalmente nos quedamos y utilizaremos.